﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Dynamic;
using System.Windows.Threading;
using System.Diagnostics;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace BehaviorFramework
{
    public class BehaviorExpando<T> : DynamicObject, INotifyPropertyChanged
    {
        private Dispatcher _uiDispatcher = Dispatcher.CurrentDispatcher;
        protected internal Dictionary<string, ObservableProperty> propertyDictionary = new Dictionary<string, ObservableProperty>();
        protected internal Dictionary<string, BehaviorCommand> commandDictionary = new Dictionary<string, BehaviorCommand>();
        private static bool isTrackingDependencies = false;
        private static List<ObservableProperty> dependencyTrail = new List<ObservableProperty>();

        public List<ObservableProperty> GetPropertyDependencies(string propertyName)
        {
            return propertyDictionary[propertyName].DependsUpon;
        }

        protected internal void RemapDependencies()
        {
            foreach (var property in propertyDictionary)
            {
				InvokeAndTrackGetter(property.Key);
            }
			foreach (var command in commandDictionary)
			{
				InvokeAndTrackCanExecute(command.Key);
			}
        }

        private dynamic InvokeAndTrackGetter(string propertyName)
        {
			var observableProperty = propertyDictionary[propertyName];

            if (!isTrackingDependencies && !observableProperty.AreDependenciesMapped )
            {
                isTrackingDependencies = true;
				observableProperty.AreDependenciesMapped = true;
                dependencyTrail = new List<ObservableProperty>();
                
                var result = observableProperty.Get.DynamicInvoke();

                observableProperty.DependsUpon = dependencyTrail;
                isTrackingDependencies = false;
                foreach (var dependency in dependencyTrail)
                {
                    var closeDep = dependency;
                    dependency.PropertyChanged += (sender, args) => observableProperty.OnPropertyChanged(closeDep);
                }
                return result;
            }
            else
            {
                dependencyTrail.Add(observableProperty);
                var result = observableProperty.Get.Invoke();
                return result;
            }
        }

        private dynamic InvokeAndTrackCanExecute(string commandName)
        {
            if (!isTrackingDependencies)
            {
                isTrackingDependencies = true;
                dependencyTrail = new List<ObservableProperty>();
                var behaviorCommand = commandDictionary[commandName];

                var result = behaviorCommand.CanExecute(null);
                behaviorCommand.DependsUpon = dependencyTrail;
                isTrackingDependencies = false;
                foreach (var dependency in dependencyTrail)
                {
                    var closeDep = dependency;
                    closeDep.PropertyChanged += (sender, args) => behaviorCommand.OnCanExecuteChanged();// CommandManager.InvalidateRequerySuggested();
                }

                return result;
            }
            else
            {
                //I don't think we need to track canexecute dependencies on other canexecute methods...
                return null;
            }
        }

        public void AddObservableProperty(string name, ObservableProperty property)
        {
            property.Name = name;
            if (propertyDictionary.ContainsKey(name))
            {
                propertyDictionary[name] = property;
            }
            else
            {
                propertyDictionary.Add(name, property);
            }

			//If this is a collection type, we need to wire up the notification that the collection has changed...
			if (property.IsCollectionType)
			{
				var collectionNotifier = (INotifyCollectionChanged)property.Get.Invoke();
				collectionNotifier.CollectionChanged += (s, a) => OnPropertyChanged(name);
			}
			else
			{
				property.PropertyChanged += (s, a) => OnPropertyChanged(name);
			}
        }

        public void AddBehaviorCommand(string name, BehaviorCommand command)
        {
            command.Name = name;
            if (commandDictionary.ContainsKey(name))
            {
                commandDictionary[name] = command;
            }
            else
            {
                commandDictionary.Add(name, command);
            }
            //InvokeAndTrackCanExecute(name);
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            object getResult;
            if (propertyDictionary.ContainsKey(binder.Name))
            {
                var observable = propertyDictionary[binder.Name];

                if (!isTrackingDependencies)
                {
                    getResult = observable.Get.Invoke();
                }
                else
                {
                    getResult = InvokeAndTrackGetter(binder.Name);
                }
                result = getResult.ChangeType(observable.ParameterType);
            }
            else if (commandDictionary.ContainsKey(binder.Name))
            {
                var command = commandDictionary[binder.Name];
                result = command;
            }
            else
            {
                result = null;
                return false;
            }

            return true;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            //if we are setting a command, then we need to wireup the canexecutechanged event when dependent
            //observables of the canexecute change...
            if (value.GetType() == typeof(BehaviorCommand))
            {
                if (commandDictionary.ContainsKey(binder.Name))
                {
                    commandDictionary.Remove(binder.Name);
                    AddBehaviorCommand(binder.Name, (BehaviorCommand)value);
                }
                else
                {
                    AddBehaviorCommand(binder.Name, (BehaviorCommand)value);
                }
            }
            //Otherwise, we add an observable property, or set the value of the observable property
            else
            {
                if (propertyDictionary.ContainsKey(binder.Name))
                {
                    var observableProperty = propertyDictionary[binder.Name];
                    observableProperty.InvokeSetter(value.ChangeType(observableProperty.ParameterType));
                }
                else if (value.GetType() == typeof(ObservableProperty))
                {
                    AddObservableProperty(binder.Name, (ObservableProperty)value);
                }
                else
                {
                    var observableProperty = new ObservableProperty(() => { return value; }, (val) => { value = val; }, value.GetType());
                    AddObservableProperty(binder.Name, observableProperty);
                }
                Debug.WriteLine("Value of property {0} set", binder.Name);
            }
            return true;
        }

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            if (commandDictionary.ContainsKey(binder.Name))
            {
                var command = commandDictionary[binder.Name];
                if (args.Count() > 0)
                {
                    command.Execute(args[0]);
                }
                else
                {
                    command.Execute(null);
                }
                result = null;
                return true;
            }
            else
            {
                throw new Exception(string.Format("Command {0} Not Found.", binder.Name));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        // Create the OnPropertyChanged method to raise the event

        protected internal void OnPropertyChanged(string name)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }
    }

}
